﻿using System;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Text;
using System.Windows.Forms;

namespace VMExplorer.Controls
{
  partial class TextArea : UserControl
  {
    #region Private fields

    MemoryDump _parent;

    #endregion

		#region Construction

		public TextArea ()
    {
      InitializeComponent ();
      this.SetStyle (ControlStyles.DoubleBuffer | ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
		}

		#endregion

		#region Public properties

		public new MemoryDump Parent 
    {
      get { return _parent; }
      set { _parent = value; }
		}

		#endregion

		#region Paint

		// Assuming a max nbCols of 128
		private StringBuilder sbPaintBytes = new StringBuilder (11 + 128 * 3 + 1);
		private StringBuilder sbPaintChars = new StringBuilder (128);

		private static bool CharIsPrintable (char c)
		{
			UnicodeCategory unicodeCategory = char.GetUnicodeCategory (c);
			if (((unicodeCategory == UnicodeCategory.Control) && (unicodeCategory != UnicodeCategory.Format)) && ((unicodeCategory != UnicodeCategory.LineSeparator) && (unicodeCategory != UnicodeCategory.ParagraphSeparator)))
			{
				return (unicodeCategory == UnicodeCategory.OtherNotAssigned);
			}
			return true;
		}

    protected override void OnPaintBackground (System.Windows.Forms.PaintEventArgs pevent)
    {
    }

    private void InvalidateSelectedRows ()
    {
      int top = (_selectionMin / Parent.BytesPerRow) * Parent.CharHeight;
      int height = ((_selectionMax / Parent.BytesPerRow) - (_selectionMin / Parent.BytesPerRow) + 1) * Parent.CharHeight;
      Invalidate (new Rectangle (0, top, Parent.Width, height));
    }

    protected override void OnPaint (PaintEventArgs pe)
    {
 			Graphics g = pe.Graphics;
			Rectangle rect = pe.ClipRectangle;

			if (rect.Width <= 0 || rect.Height <= 0) {
				return;
			}

			ByteBuffer buffer = Parent.ByteBuffer;
      if (buffer == null) return;

			int nbCols = Parent.BytesPerRow;

			int firstRow = rect.Top / Parent.CharHeight;
			int lastRow = rect.Bottom / Parent.CharHeight + 1;

      // Paint background and lines
      int currentColumnOffset = 0;
      g.FillRectangle (Brushes.WhiteSmoke, new Rectangle (currentColumnOffset, rect.Top, Parent.AddressColumnWidth, rect.Height));
      currentColumnOffset += Parent.AddressColumnWidth;
      g.FillRectangle (SystemBrushes.Window, new Rectangle (currentColumnOffset, rect.Top, Parent.BytesColumnWidth, rect.Height));
      g.DrawLine (Pens.LightGray, new Point (currentColumnOffset, rect.Top), new Point (currentColumnOffset, rect.Bottom));
      currentColumnOffset += Parent.BytesColumnWidth;
      g.FillRectangle (Brushes.WhiteSmoke, new Rectangle (currentColumnOffset, rect.Top, this.Width - currentColumnOffset, rect.Height));
      g.DrawLine (Pens.LightGray, new Point (currentColumnOffset, rect.Top), new Point (currentColumnOffset, rect.Bottom)); 

			int currentByte = 0;
			Point currentLine = new Point (1, 0);

      const TextFormatFlags textFormatFlags = TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix | TextFormatFlags.PreserveGraphicsClipping;

			currentLine.X = 1;
      currentLine.Y += (firstRow-1)*Parent.CharHeight;
			currentByte += (firstRow-1)*nbCols;

      for (int row = firstRow; row < lastRow; row++)
			{
        // Draw address
        TextRenderer.DrawText (g, (Parent.StartAddress + currentByte).ToString (), Parent.BytesFont, currentLine, Parent.BytesForeColor, textFormatFlags);
        currentLine.X += Parent.AddressColumnWidth + Parent.CharWidth - 1;

        // Draw bytes
        int col = 0;
				while (col < nbCols)
				{
          Color foreColor = Parent.BytesForeColor;
					Color backColor = SystemColors.Window;

					bool isCurrentValid = (currentByte >= buffer.Start && currentByte < buffer.End);
          bool isCurrentSelected = (currentByte >= _selectionMin && currentByte < _selectionMax);

          bool isLastByteOfRow = (col == nbCols - 1);
					bool isNextValid = (currentByte+1 >= buffer.Start && currentByte+1 < buffer.End);
          bool isNextSelected = (currentByte+1 >= _selectionMin && currentByte+1 < _selectionMax);

					// format strings
					if (isCurrentValid)
					{
						byte b = buffer.Data [currentByte - buffer.Start];
						sbPaintBytes.AppendFormat ("{0:X2}", b);

						char c = Convert.ToChar (b);
						sbPaintChars.Append (CharIsPrintable (c) ? c : '.');
					}
					else
					{
						sbPaintBytes.Append ("??");
						sbPaintChars.Append (' ');
					}

          if(!isLastByteOfRow && isCurrentSelected && isNextSelected)
					  sbPaintBytes.Append (' ');

          if (isCurrentSelected)
					{
						if (isCurrentValid)
						{
							foreColor = SystemColors.HighlightText;
							backColor = SystemColors.Highlight;
						}
						else
						{
							foreColor = SystemColors.HighlightText;
							backColor = SystemColors.Highlight;
						}
					}
					else
					{
						if (isCurrentValid)
						{
							foreColor = Parent.BytesForeColor;
							backColor = SystemColors.Window;
						}
						else
						{
							foreColor = Color.LightGray;
							backColor = SystemColors.Window;
						}
					}

          TextRenderer.DrawText (g, sbPaintBytes.ToString (), Parent.BytesFont, currentLine, foreColor, backColor, textFormatFlags);

					sbPaintBytes.Remove (0, sbPaintBytes.Length);
          currentLine.X += Parent.OneByteWidth + Parent.InterByteWidth;
					currentByte++;
          col++;
				}

        // Draw ASCII
        currentLine.X += Parent.CharWidth;
        TextRenderer.DrawText (g, sbPaintChars.ToString (), Parent.BytesFont, currentLine, Parent.BytesForeColor, textFormatFlags);


				sbPaintBytes.Remove (0, sbPaintBytes.Length);
				sbPaintChars.Remove (0, sbPaintChars.Length);

				currentLine.X = 1;
        currentLine.Y += Parent.CharHeight;
			}

      base.OnPaint (pe);
		}

		#endregion

    #region Mouse

		private int _selectionFirst = -1;
		private int _selectionMin = -1;
		private int _selectionMax = -1;
    private bool _isSelecting = false;

    private Point MouseToByteLocation (Point mouseLocation)
    {
      int col = (mouseLocation.X - Parent.AddressColumnWidth + Parent.CharWidth) / (3 * Parent.CharWidth);
      int row = (mouseLocation.Y / Parent.CharHeight);
      return new Point (col, row);
    }

    private void TextArea_MouseDown (object sender, MouseEventArgs e)
    {
      Rectangle bytesZone = new Rectangle (Parent.AddressColumnWidth, 0, Parent.BytesColumnWidth, this.Height);
      if (bytesZone.Contains (e.Location))
      {
        // Invalidate previous selection
        InvalidateSelectedRows ();

        Point byteLocation = MouseToByteLocation (e.Location);

        _selectionFirst = byteLocation.Y * Parent.BytesPerRow + byteLocation.X;
        _selectionMin = _selectionFirst;
        _selectionMax = _selectionFirst;

        _isSelecting = true;

        Update ();
      }
    }

    private void TextArea_MouseMove (object sender, MouseEventArgs e)
    {
      Rectangle bytesZone = new Rectangle (Parent.AddressColumnWidth, 0, Parent.BytesColumnWidth, this.Height);
      if (bytesZone.Contains (e.Location))
      {
        this.Cursor = Cursors.IBeam;

        if (_isSelecting)
        {
          // Invalidate previous selection
          InvalidateSelectedRows ();

          Point byteLocation = MouseToByteLocation (e.Location);

          int newSelection = byteLocation.Y * Parent.BytesPerRow + byteLocation.X;
          if (newSelection < _selectionFirst)
          {
            _selectionMin = newSelection;
            _selectionMax = _selectionFirst;
          }
          else
          {
            _selectionMin = _selectionFirst;
            _selectionMax = newSelection;
          }

          // Invalidate new selection
          InvalidateSelectedRows ();

          Update ();
        }
      }
      else
      {
        this.Cursor = Cursors.Default;
      }
    }

    private void TextArea_MouseUp (object sender, MouseEventArgs e)
    {
      Rectangle bytesZone = new Rectangle (Parent.AddressColumnWidth, 0, Parent.BytesColumnWidth, this.Height);
      if (bytesZone.Contains (e.Location))
      {
        _isSelecting = false;
      }
    }

    #endregion
  }
}
